/* -*- mode: jde; c-basic-offset: 2; indent-tabs-mode: nil -*- */

/*
 Editor - main editor panel for the processing development environment
 Part of the Processing project - http://processing.org
 
 Copyright (c) 2004-05 Ben Fry and Casey Reas
 Copyright (c) 2001-04 Massachusetts Institute of Technology
 
 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or
 (at your option) any later version.
 
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software Foundation,
 Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
 $Id: Editor.java 370 2008-01-19 16:37:19Z mellis $
 */

package processing.app;

import processing.app.syntax.*;
import processing.app.tools.*;
import processing.core.*;

import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.dnd.*;
import java.awt.event.*;
import java.awt.print.*;
import java.io.*;
import java.lang.reflect.*;
import java.net.*;
import java.util.*;
import java.util.zip.*;

import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.undo.*;

import com.apple.mrj.*;
import com.oroinc.text.regex.*;
//import de.hunsicker.jalopy.*;

import com.apple.mrj.*;
import gnu.io.*;

public class Editor extends JFrame
  implements MRJAboutHandler, MRJQuitHandler, MRJPrefsHandler,
	MRJOpenDocumentHandler //, MRJOpenApplicationHandler
	{
		// yeah
		static final String WINDOW_TITLE = "Mididuino" + " - " + Base.VERSION_NAME;
		
		// p5 icon for the window
		Image icon;
		
		// otherwise, if the window is resized with the message label
		// set to blank, it's preferredSize() will be fukered
		static public final String EMPTY =
    "                                                                     " +
    "                                                                     " +
    "                                                                     ";
		
		static public final KeyStroke WINDOW_CLOSE_KEYSTROKE =
    KeyStroke.getKeyStroke('W', Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
		
		static final int HANDLE_NEW  = 1;
		static final int HANDLE_OPEN = 2;
		static final int HANDLE_QUIT = 3;
		int checkModifiedMode;
		String handleOpenPath;
		boolean handleNewShift;
		boolean handleNewLibrary;
		
		PageFormat pageFormat;
		PrinterJob printerJob;
		
		EditorButtons buttons;
		EditorHeader header;
		EditorStatus status;
		EditorConsole console;
		Serial serialPort;
		JSplitPane splitPane;
		JPanel consolePanel;
		
		JLabel lineNumberComponent;
		
		// currently opened program
		public Sketch sketch;
		
		EditorLineStatus lineStatus;
		
		public JEditTextArea textarea;
		EditorListener listener;
		
		// runtime information and window placement
		Point appletLocation;
		//Point presentLocation;
		//Window presentationWindow;
		RunButtonWatcher watcher;
		//Runner runtime;
		
		
		JMenuItem exportAppItem;
		JMenuItem saveMenuItem;
		JMenuItem saveAsMenuItem;
		
		JMenuItem burnBootloader8Item = null;
		JMenuItem burnBootloader8ParallelItem = null;
		JMenuItem burnBootloader168DiecimilaItem = null;
		JMenuItem burnBootloader168DiecimilaParallelItem = null;
		JMenuItem burnBootloader168NGItem = null;
		JMenuItem burnBootloader168NGParallelItem = null;
		
		JMenu serialMenu;
		JMenu midiMenu;
		JMenu midiInputMenu;
		JMenu midiOutputMenu;
		JMenu serialRateMenu;
		JMenu mcuMenu;
		
		SerialMenuListener serialMenuListener;
		MidiInputMenuListener midiInputMenuListener;
		MidiOutputMenuListener midiOutputMenuListener;
		
		boolean running;
		boolean presenting;
		boolean debugging;
		
		// undo fellers
		JMenuItem undoItem, redoItem;
		protected UndoAction undoAction;
		protected RedoAction redoAction;
		UndoManager undo;
		// used internally, and only briefly
		CompoundEdit compoundEdit;
		
		//
		
		//SketchHistory history;  // TODO re-enable history
		Sketchbook sketchbook;
		//Preferences preferences;
		FindReplace find;
		
		//static Properties keywords; // keyword -> reference html lookup
		
		
		public Editor() {
			super(WINDOW_TITLE);
			
			// #@$*(@#$ apple.. always gotta think different
			MRJApplicationUtils.registerAboutHandler(this);
			MRJApplicationUtils.registerPrefsHandler(this);
			MRJApplicationUtils.registerQuitHandler(this);
			MRJApplicationUtils.registerOpenDocumentHandler(this);
			
			// run static initialization that grabs all the prefs
			Preferences.init();
			
			// set the window icon
			try {
				icon = Base.getImage("icon.gif", this);
				setIconImage(icon);
			} catch (Exception e) { } // fail silently, no big whup
			
			
			// add listener to handle window close box hit event
			addWindowListener(new WindowAdapter() {
												public void windowClosing(WindowEvent e) {
												handleQuitInternal();
												}
												});
			// don't close the window when clicked, the app will take care
			// of that via the handleQuitInternal() methods
			// http://dev.processing.org/bugs/show_bug.cgi?id=440
			setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
			
			PdeKeywords keywords = new PdeKeywords();
			sketchbook = new Sketchbook(this);
			
			JMenuBar menubar = new JMenuBar();
			menubar.add(buildFileMenu());
			menubar.add(buildEditMenu());
			menubar.add(buildSketchMenu());
			menubar.add(buildToolsMenu());
			// what platform has their help menu way on the right? motif?
			//menubar.add(Box.createHorizontalGlue());
			menubar.add(buildHelpMenu());
			
			setJMenuBar(menubar);
			
			// doesn't matter when this is created, just make it happen at some point
			//find = new FindReplace(Editor.this);
			
			//Container pain = getContentPane();
			//pain.setLayout(new BorderLayout());
			// for rev 0120, placing things inside a JPanel because
			Container contentPain = getContentPane();
			contentPain.setLayout(new BorderLayout());
			JPanel pain = new JPanel();
			pain.setLayout(new BorderLayout());
			contentPain.add(pain, BorderLayout.CENTER);
			
			Box box = Box.createVerticalBox();
			Box upper = Box.createVerticalBox();
			
			buttons = new EditorButtons(this);
			upper.add(buttons);
			
			header = new EditorHeader(this);
			//header.setBorder(null);
			upper.add(header);
			
			textarea = new JEditTextArea(new PdeTextAreaDefaults());
			textarea.setRightClickPopup(new TextAreaPopup());
			//textarea.setTokenMarker(new PdeKeywords());
			textarea.setHorizontalOffset(6);
			
			// assemble console panel, consisting of status area and the console itself
			consolePanel = new JPanel();
			consolePanel.setLayout(new BorderLayout());
			
			status = new EditorStatus(this);
			consolePanel.add(status, BorderLayout.NORTH);
			
			console = new EditorConsole(this);
			// windows puts an ugly border on this guy
			console.setBorder(null);
			consolePanel.add(console, BorderLayout.CENTER);
			
			lineStatus = new EditorLineStatus(textarea);
			consolePanel.add(lineStatus, BorderLayout.SOUTH);
			
			upper.add(textarea);
			splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
																 upper, consolePanel);
			//textarea, consolePanel);
			
			splitPane.setOneTouchExpandable(true);
			// repaint child panes while resizing
			splitPane.setContinuousLayout(true);
			// if window increases in size, give all of increase to
			// the textarea in the uppper pane
			splitPane.setResizeWeight(1D);
			
			// to fix ugliness.. normally macosx java 1.3 puts an
			// ugly white border around this object, so turn it off.
			splitPane.setBorder(null);
			
			// the default size on windows is too small and kinda ugly
			int dividerSize = Preferences.getInteger("editor.divider.size");
			if (dividerSize != 0) {
				splitPane.setDividerSize(dividerSize);
			}
			
			splitPane.setMinimumSize(new Dimension(600, 600));
			box.add(splitPane);
			
			// hopefully these are no longer needed w/ swing
			// (har har har.. that was wishful thinking)
			listener = new EditorListener(this, textarea);
			pain.add(box);
			
			pain.setTransferHandler(new TransferHandler() {
															
															public boolean canImport(JComponent dest, DataFlavor[] flavors) {
															// claim that we can import everything
															return true;
															}
															
															public boolean importData(JComponent src, Transferable transferable) {
															DataFlavor[] flavors = transferable.getTransferDataFlavors();
															
															/*
			 DropTarget dt = new DropTarget(this, new DropTargetListener() {
			 
			 public void dragEnter(DropTargetDragEvent event) {
			 // debug messages for diagnostics
			 //System.out.println("dragEnter " + event);
			 event.acceptDrag(DnDConstants.ACTION_COPY);
			 }
			 
			 public void dragExit(DropTargetEvent event) {
			 //System.out.println("dragExit " + event);
			 }
			 
			 public void dragOver(DropTargetDragEvent event) {
			 //System.out.println("dragOver " + event);
			 event.acceptDrag(DnDConstants.ACTION_COPY);
			 }
			 
			 public void dropActionChanged(DropTargetDragEvent event) {
			 //System.out.println("dropActionChanged " + event);
			 }
			 
			 public void drop(DropTargetDropEvent event) {
			 //System.out.println("drop " + event);
			 event.acceptDrop(DnDConstants.ACTION_COPY);
			 
			 Transferable transferable = event.getTransferable();
			 DataFlavor flavors[] = transferable.getTransferDataFlavors();
			 */
															int successful = 0;
															
															for (int i = 0; i < flavors.length; i++) {
															try {
															//System.out.println(flavors[i]);
															//System.out.println(transferable.getTransferData(flavors[i]));
															Object stuff = transferable.getTransferData(flavors[i]);
															if (!(stuff instanceof java.util.List)) continue;
															java.util.List list = (java.util.List) stuff;
															
															for (int j = 0; j < list.size(); j++) {
															Object item = list.get(j);
															if (item instanceof File) {
															File file = (File) item;
															
															// see if this is a .pde file to be opened
															String filename = file.getName();
															if (filename.endsWith(".pde")) {
															String name = filename.substring(0, filename.length() - 4);
															File parent = file.getParentFile();
															if (name.equals(parent.getName())) {
															handleOpenFile(file);
															return true;
															}
															}
															
															if (sketch.addFile(file)) {
															successful++;
															}
															}
															}
															
															} catch (Exception e) {
															e.printStackTrace();
															return false;
															}
															}
															
															if (successful == 0) {
															error("No files were added to the sketch.");
															
															} else if (successful == 1) {
															message("One file added to the sketch.");
															
															} else {
															message(successful + " files added to the sketch.");
															}
															return true;
															}
															});
		}
		
		
		/**
		 * Hack for #@#)$(* Mac OS X 10.2.
		 * <p/>
		 * This appears to only be required on OS X 10.2, and is not
		 * even being called on later versions of OS X or Windows.
		 */
		public Dimension getMinimumSize() {
			//System.out.println("getting minimum size");
			return new Dimension(500, 550);
		}
		
		
		// ...................................................................
		
		/**
		 * Builds any unbuilt buildable libraries
		 * Adds syntax coloring from those libraries (if exists)
		 * Rebuilds sketchbook menu with library examples (if they exist)
		 */
		public void prepareLibraries() {
			// build any unbuilt libraries
			try {
				LibraryManager libraryManager = new LibraryManager();
				libraryManager.buildAllUnbuilt();
				// update syntax coloring table
				libraryManager.addSyntaxColoring(new PdeKeywords());
			} catch (RunnerException re) {
				message("Error compiling library ...");
				error(re);
			} catch (Exception ex) {
				ex.printStackTrace();
			}
			// update sketchbook menu, this adds examples of any built libs
			sketchbook.rebuildMenus();
		}
		
		// ...................................................................
		
		/**
		 * Post-constructor setup for the editor area. Loads the last
		 * sketch that was used (if any), and restores other Editor settings.
		 * The complement to "storePreferences", this is called when the
		 * application is first launched.
		 */
		public void restorePreferences() {
			// figure out window placement
			
			Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
			boolean windowPositionValid = true;
			
			if (Preferences.get("last.screen.height") != null) {
				// if screen size has changed, the window coordinates no longer
				// make sense, so don't use them unless they're identical
				int screenW = Preferences.getInteger("last.screen.width");
				int screenH = Preferences.getInteger("last.screen.height");
				
				if ((screen.width != screenW) || (screen.height != screenH)) {
					windowPositionValid = false;
				}
				int windowX = Preferences.getInteger("last.window.x");
				int windowY = Preferences.getInteger("last.window.y");
				if ((windowX < 0) || (windowY < 0) ||
						(windowX > screenW) || (windowY > screenH)) {
					windowPositionValid = false;
				}
				
			} else {
				windowPositionValid = false;
			}
			
			if (!windowPositionValid) {
				//System.out.println("using default size");
				int windowH = Preferences.getInteger("default.window.height");
				int windowW = Preferences.getInteger("default.window.width");
				setBounds((screen.width - windowW) / 2,
									(screen.height - windowH) / 2,
									windowW, windowH);
				// this will be invalid as well, so grab the new value
				Preferences.setInteger("last.divider.location",
															 splitPane.getDividerLocation());
			} else {
				setBounds(Preferences.getInteger("last.window.x"),
									Preferences.getInteger("last.window.y"),
									Preferences.getInteger("last.window.width"),
									Preferences.getInteger("last.window.height"));
			}
			
			
			// last sketch that was in use, or used to launch the app
			
			if (Base.openedAtStartup != null) {
				handleOpen2(Base.openedAtStartup);
				
			} else {
				//String sketchName = Preferences.get("last.sketch.name");
				String sketchPath = Preferences.get("last.sketch.path");
				//Sketch sketchTemp = new Sketch(sketchPath);
				
				if ((sketchPath != null) && (new File(sketchPath)).exists()) {
					// don't check modified because nothing is open yet
					handleOpen2(sketchPath);
					
				} else {
					handleNew2(true);
				}
			}
			
			
			// location for the console/editor area divider
			
			int location = Preferences.getInteger("last.divider.location");
			splitPane.setDividerLocation(location);
			
			
			// read the preferences that are settable in the preferences window
			
			applyPreferences();
		}
		
		
		/**
		 * Read and apply new values from the preferences, either because
		 * the app is just starting up, or the user just finished messing
		 * with things in the Preferences window.
		 */
		public void applyPreferences() {
			
			// apply the setting for 'use external editor'
			boolean external = Preferences.getBoolean("editor.external");
			
			textarea.setEditable(!external);
			saveMenuItem.setEnabled(!external);
			saveAsMenuItem.setEnabled(!external);
			//beautifyMenuItem.setEnabled(!external);
			
			TextAreaPainter painter = textarea.getPainter();
			if (external) {
				// disable line highlight and turn off the caret when disabling
				Color color = Preferences.getColor("editor.external.bgcolor");
				painter.setBackground(color);
				painter.setLineHighlightEnabled(false);
				textarea.setCaretVisible(false);
				
			} else {
				Color color = Preferences.getColor("editor.bgcolor");
				painter.setBackground(color);
				boolean highlight = Preferences.getBoolean("editor.linehighlight");
				painter.setLineHighlightEnabled(highlight);
				textarea.setCaretVisible(true);
			}
			
			// apply changes to the font size for the editor
			//TextAreaPainter painter = textarea.getPainter();
			painter.setFont(Preferences.getFont("editor.font"));
			//Font font = painter.getFont();
			//textarea.getPainter().setFont(new Font("Courier", Font.PLAIN, 36));
			
			// in case tab expansion stuff has changed
			listener.applyPreferences();
			
			// in case moved to a new location
			// For 0125, changing to async version (to be implemented later)
			//sketchbook.rebuildMenus();
			sketchbook.rebuildMenusAsync();
		}
		
		
		/**
		 * Store preferences about the editor's current state.
		 * Called when the application is quitting.
		 */
		public void storePreferences() {
			//System.out.println("storing preferences");
			
			// window location information
			Rectangle bounds = getBounds();
			Preferences.setInteger("last.window.x", bounds.x);
			Preferences.setInteger("last.window.y", bounds.y);
			Preferences.setInteger("last.window.width", bounds.width);
			Preferences.setInteger("last.window.height", bounds.height);
			
			Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
			Preferences.setInteger("last.screen.width", screen.width);
			Preferences.setInteger("last.screen.height", screen.height);
			
			// last sketch that was in use
			//Preferences.set("last.sketch.name", sketchName);
			//Preferences.set("last.sketch.name", sketch.name);
			Preferences.set("last.sketch.path", sketch.getMainFilePath());
			
			// location for the console/editor area divider
			int location = splitPane.getDividerLocation();
			Preferences.setInteger("last.divider.location", location);
		}
		
		
		// ...................................................................
		
		
		protected JMenu buildFileMenu() {
			JMenuItem item;
			JMenu menu = new JMenu("File");
			
			item = newJMenuItem("New", 'N');
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 handleNew(false);
														 }
														 });
			menu.add(item);
			menu.add(sketchbook.getOpenMenu());
			
			saveMenuItem = newJMenuItem("Save", 'S');
			saveMenuItem.addActionListener(new ActionListener() {
																		 public void actionPerformed(ActionEvent e) {
																		 handleSave(false);
																		 }
																		 });
			menu.add(saveMenuItem);
			
			saveAsMenuItem = newJMenuItem("Save As...", 'S', true);
			saveAsMenuItem.addActionListener(new ActionListener() {
																			 public void actionPerformed(ActionEvent e) {
																			 handleSaveAs();
																			 }
																			 });
			menu.add(saveAsMenuItem);
			
			item = newJMenuItem("Upload to I/O Board", 'U');
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 handleExport();
														 }
														 });
			menu.add(item);
			
			/*exportAppItem = newJMenuItem("Export Application", 'E', true);
			 exportAppItem.addActionListener(new ActionListener() {
			 public void actionPerformed(ActionEvent e) {
			 //buttons.activate(EditorButtons.EXPORT);
			 //SwingUtilities.invokeLater(new Runnable() {
			 //public void run() {
			 handleExportApplication();
			 //}});
			 }
			 });
			 menu.add(exportAppItem);
			 */
			menu.addSeparator();
			
			item = newJMenuItem("Page Setup", 'P', true);
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 handlePageSetup();
														 }
														 });
			menu.add(item);
			
			item = newJMenuItem("Print", 'P');
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 handlePrint();
														 }
														 });
			menu.add(item);
			
			// macosx already has its own preferences and quit menu
			if (!Base.isMacOS()) {
				menu.addSeparator();
				
				item = newJMenuItem("Preferences", ',');
				item.addActionListener(new ActionListener() {
															 public void actionPerformed(ActionEvent e) {
															 handlePrefs();
															 }
															 });
				menu.add(item);
				
				menu.addSeparator();
				
				item = newJMenuItem("Quit", 'Q');
				item.addActionListener(new ActionListener() {
															 public void actionPerformed(ActionEvent e) {
															 handleQuitInternal();
															 }
															 });
				menu.add(item);
			}
			return menu;
		}
		
		
		protected JMenu buildSketchMenu() {
			JMenuItem item;
			JMenu menu = new JMenu("Sketch");
			
			item = newJMenuItem("Verify/Compile", 'R');
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 handleRun(false);
														 }
														 });
			menu.add(item);
			
			/*item = newJMenuItem("Present", 'R', true);
			 item.addActionListener(new ActionListener() {
			 public void actionPerformed(ActionEvent e) {
			 handleRun(true);
			 }
			 });
			 menu.add(item);
			 */
			item = new JMenuItem("Stop");
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 handleStop();
														 }
														 });
			menu.add(item);
			
			menu.addSeparator();
			
			menu.add(sketchbook.getImportMenu());
			
			//if (Base.isWindows() || Base.isMacOS()) {
      // no way to do an 'open in file browser' on other platforms
      // since there isn't any sort of standard
      item = newJMenuItem("Show Sketch Folder", 'K', false);
      item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 //Base.openFolder(sketchDir);
														 Base.openFolder(sketch.folder);
														 }
														 });
      menu.add(item);
			if (!Base.openFolderAvailable()) {
				item.setEnabled(false);
			}
			
			//menu.addSeparator();
			
			item = new JMenuItem("Add File...");
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 sketch.addFile();
														 }
														 });
			menu.add(item);
			
			// TODO re-enable history
			//history.attachMenu(menu);
			return menu;
		}
		
		
		protected JMenu buildToolsMenu() {
			JMenuItem item;
			JMenuItem rbMenuItem;
			JMenuItem cbMenuItem;
			
			serialMenuListener  = new SerialMenuListener();
			midiInputMenuListener = new MidiInputMenuListener();
			midiOutputMenuListener = new MidiOutputMenuListener();
			
			JMenu menu = new JMenu("Tools");
			
			item = newJMenuItem("Auto Format", 'T', false);
			item.addActionListener(new ActionListener() {
														 synchronized public void actionPerformed(ActionEvent e) {
														 new AutoFormat(Editor.this).show();
														 
														 /*
			 Jalopy jalopy = new Jalopy();
			 jalopy.setInput(getText(), sketch.current.file.getAbsolutePath());
			 StringBuffer buffer = new StringBuffer();
			 jalopy.setOutput(buffer);
			 jalopy.setInspect(false);
			 jalopy.format();
			 setText(buffer.toString(), 0, 0);
			 
			 if (jalopy.getState() == Jalopy.State.OK)
			 System.out.println("successfully formatted");
			 else if (jalopy.getState() == Jalopy.State.WARN)
			 System.out.println(" formatted with warnings");
			 else if (jalopy.getState() == Jalopy.State.ERROR)
			 System.out.println(" could not be formatted");
			 */
														 }
														 });
			menu.add(item);
			
			item = new JMenuItem("Copy for Forum");
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 SwingUtilities.invokeLater(new Runnable() {
																												public void run() {
																												new DiscourseFormat(Editor.this).show();
																												}
																												});
														 }
														 });
			menu.add(item);
			
			item = new JMenuItem("Archive Sketch");
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 new Archiver(Editor.this).show();
														 //Archiver archiver = new Archiver();
														 //archiver.setup(Editor.this);
														 //archiver.show();
														 }
														 });
			menu.add(item);
			
			/*
			 item = new JMenuItem("Export Folder...");
			 item.addActionListener(new ActionListener() {
			 public void actionPerformed(ActionEvent e) {
			 SwingUtilities.invokeLater(new Runnable() {
			 public void run() {
			 new ExportFolder(Editor.this).show();
			 }
			 });
			 menu.add(item);
			 */
			menu.addSeparator();
			
			JMenu boardsMenu = new JMenu("Board");
			ButtonGroup boardGroup = new ButtonGroup();
			for (Iterator i = Preferences.getSubKeys("boards"); i.hasNext(); ) {
				String board = (String) i.next();
				Action action = new BoardMenuAction(board);
				item = new JRadioButtonMenuItem(action);
				if (board.equals(Preferences.get("board")))
					item.setSelected(true);
				boardGroup.add(item);
				boardsMenu.add(item);
			}
			menu.add(boardsMenu);
			
//			serialMenu = new JMenu("Serial Port");
//			populateSerialMenu();
//			menu.add(serialMenu);
			
			
			midiMenu = new JMenu("MIDI Ports");
			populateMidiMenu();
			menu.add(midiMenu);
			
			menu.addSeparator();
			
			JMenu bootloaderMenu = new JMenu("Burn Bootloader");
			for (Iterator i = Preferences.getSubKeys("programmers"); i.hasNext(); ) {
				String programmer = (String) i.next();
				Action action = new BootloaderMenuAction(programmer);
				item = new JMenuItem(action);
				bootloaderMenu.add(item);
			}
			menu.add(bootloaderMenu);

			/*
			menu.addMenuListener(new MenuListener() {
													 public void menuCanceled(MenuEvent e) {}
													 public void menuDeselected(MenuEvent e) {}
													 public void menuSelected(MenuEvent e) {
													 //System.out.println("Tools menu selected.");
													 populateSerialMenu();
													 }});
			 */
			
			
			return menu;
		}
		
		class MidiMenuListener implements ActionListener {
			protected String prefName = "midi.port";
			
			protected JMenu getMenu() {
				return midiMenu;
			}

			public void actionPerformed(ActionEvent e) {
				if (getMenu() == null) {
					System.out.println("midiMenu is null");
					return;
				}
				
				JMenu midiMenu = getMenu();
				int count = midiMenu.getItemCount();
				for (int i = 0; i < count; i++) {
					if (midiMenu.getItem(i) instanceof JCheckBoxMenuItem) {
						((JCheckBoxMenuItem)midiMenu.getItem(i)).setState(false);
					}
				}
				JCheckBoxMenuItem item = (JCheckBoxMenuItem)e.getSource();
				item.setState(true);
				String name = item.getText();
				//System.out.println(item.getLabel());
				Preferences.set(prefName, name);
				System.out.println("MIDI " + prefName + " set to " + Preferences.get(prefName));
				
			}
		}
		
		class MidiInputMenuListener extends MidiMenuListener {
			protected JMenu getMenu() {
				return midiInputMenu;
			}
			
			MidiInputMenuListener() {
				super();
				prefName = "midi.inputport";
			}
		}
		
		class MidiOutputMenuListener extends MidiMenuListener {
			protected JMenu getMenu() {
				return midiOutputMenu;
			}
			
			MidiOutputMenuListener() {
				super();
				prefName = "midi.outputport";
			}
		}
		
		class SerialMenuListener implements ActionListener {
			//public SerialMenuListener() { }
			
			public void actionPerformed(ActionEvent e) {
				if(serialMenu == null) {
					System.out.println("serialMenu is null");
					return;
				}
				int count = serialMenu.getItemCount();
				for (int i = 0; i < count; i++) {
					((JCheckBoxMenuItem)serialMenu.getItem(i)).setState(false);
				}
				JCheckBoxMenuItem item = (JCheckBoxMenuItem)e.getSource();
				item.setState(true);
				String name = item.getText();
				//System.out.println(item.getLabel());
				Preferences.set("serial.port", name);
				//System.out.println("set to " + get("serial.port"));
			}
			
			/*
			 public void actionPerformed(ActionEvent e) {
			 System.out.println(e.getSource());
			 String name = e.getActionCommand();
			 PdeBase.properties.put("serial.port", name);
			 System.out.println("set to " + get("serial.port"));
			 //editor.skOpen(path + File.separator + name, name);
			 // need to push "serial.port" into PdeBase.properties
			 }
			 */
		}
		
		class BoardMenuAction extends AbstractAction {
			private String board;
			public BoardMenuAction(String board) {
				super(Preferences.get("boards." + board + ".name"));
				this.board = board;
			}
			public void actionPerformed(ActionEvent actionevent) {
				//System.out.println("Switching to " + board);
				Preferences.set("board", board);
				try {
					LibraryManager libraryManager = new LibraryManager();
					libraryManager.rebuildAllBuilt();
				} catch (IOException e) {
					e.printStackTrace();
				} catch (RunnerException e) {
					message("Error rebuilding libraries...");
					error(e);
				}
			}
		}
		
		class BootloaderMenuAction extends AbstractAction {
			private String programmer;
			public BootloaderMenuAction(String programmer) {
				super("w/ " + Preferences.get("programmers." + programmer + ".name"));
				this.programmer = programmer;
			}
			public void actionPerformed(ActionEvent actionevent) {
				handleBurnBootloader(programmer);
			}
		}
		
		protected void populateMidiMenu() {
			JMenuItem rbMenuItem;
			
			midiMenu.removeAll();
			boolean empty = true;

			MidiUploader uploader = new MidiUploader();
			
			midiInputMenu = new JMenu("Input port");
			midiOutputMenu = new JMenu("Output port");
			
			rbMenuItem = new JMenuItem("Rescan MIDI Ports");
			rbMenuItem.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 SwingUtilities.invokeLater(new Runnable() {
																												public void run() {
																												populateMidiMenu();
																												}
																												});
														 }
														 });
			midiMenu.add(rbMenuItem);
			midiMenu.addSeparator();

			midiMenu.add(midiInputMenu);
			midiMenu.add(midiOutputMenu);
			
			String ports[];
			
			ports = uploader.getMidiInputPorts();

			if (ports != null) {
				System.out.println("ports input " + ports.length);
				for (int i = 0; i < ports.length; i++) {
					String curport = ports[i];
					rbMenuItem = new JCheckBoxMenuItem(curport, curport.equals(Preferences.get("midi.inputport")));
					rbMenuItem.addActionListener(midiInputMenuListener);
					//serialGroup.add(rbMenuItem);
					midiInputMenu.add(rbMenuItem);
					empty = false;
				}
			}
			
			try {
				Thread.sleep(1000); // sleep to catch midi weirdness under macosx
			} catch (Exception e) {
			}
			
			ports = uploader.getMidiOutputPorts();
			if (ports != null) {
				System.out.println("ports output " + ports.length);
				for (int i = 0; i < ports.length; i++) {
					String curport = ports[i];
					rbMenuItem = new JCheckBoxMenuItem(curport, curport.equals(Preferences.get("midi.outputport")));
					rbMenuItem.addActionListener(midiOutputMenuListener);
					//serialGroup.add(rbMenuItem);
					midiOutputMenu.add(rbMenuItem);
					empty = false;
				}
			}
			
			
			if (!empty) {
				//System.out.println("enabling the midiMenu");
				midiMenu.setEnabled(true);
			}
		}
		
		protected void populateSerialMenu() {
			// getting list of ports
			
			JMenuItem rbMenuItem;
			
			//System.out.println("Clearing serial port menu.");
			
			serialMenu.removeAll();
			boolean empty = true;
			
			try
			{
				for (Enumeration enumeration = CommPortIdentifier.getPortIdentifiers(); enumeration.hasMoreElements();)
				{
					CommPortIdentifier commportidentifier = (CommPortIdentifier)enumeration.nextElement();
					//System.out.println("Found communication port: " + commportidentifier);
					if (commportidentifier.getPortType() == CommPortIdentifier.PORT_SERIAL)
					{
						//System.out.println("Adding port to serial port menu: " + commportidentifier);
						String curr_port = commportidentifier.getName();
						rbMenuItem = new JCheckBoxMenuItem(curr_port, curr_port.equals(Preferences.get("serial.port")));
						rbMenuItem.addActionListener(serialMenuListener);
						//serialGroup.add(rbMenuItem);
						serialMenu.add(rbMenuItem);
						empty = false;
					}
				}
				if (!empty) {
					//System.out.println("enabling the serialMenu");
					serialMenu.setEnabled(true);
				}
				
			}
			
			catch (Exception exception)
			{
				System.out.println("error retrieving port list");
				exception.printStackTrace();
			}
			
			if (serialMenu.getItemCount() == 0) {
				serialMenu.setEnabled(false);
			}
			
			//serialMenu.addSeparator();
			//serialMenu.add(item);
		}
		
		protected JMenu buildHelpMenu() {
			JMenu menu = new JMenu("Help");
			JMenuItem item;
			
			/*
			if (!Base.isLinux()) {
				item = new JMenuItem("Getting Started");
				item.addActionListener(new ActionListener() {
															 public void actionPerformed(ActionEvent e) {
															 if (Base.isWindows())
															 Base.openURL(System.getProperty("user.dir") + File.separator +
																						"reference" + File.separator + "Guide_Windows.html");
															 else
															 Base.openURL(System.getProperty("user.dir") + File.separator +
																						"reference" + File.separator + "Guide_MacOSX.html");
															 }
															 });
				menu.add(item);
			}
			
			item = new JMenuItem("Environment");
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 Base.showEnvironment();
														 }
														 });
			menu.add(item);
			
			item = new JMenuItem("Troubleshooting");
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 Base.showTroubleshooting();
														 }
														 });
			menu.add(item);
			
			item = new JMenuItem("Reference");
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 Base.showReference();
														 }
														 });
			menu.add(item);
			
			item = newJMenuItem("Find in Reference", 'F', true);
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 if (textarea.isSelectionActive()) {
														 handleReference();
														 }
														 }
														 });
			menu.add(item);
			
			item = new JMenuItem("Frequently Asked Questions");
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 Base.showFAQ();
														 }
														 });
			menu.add(item);
			 */
			
			item = new JMenuItem("Open user manual");
			item.addActionListener(new ActionListener() {
								   public void actionPerformed(ActionEvent e) {
								   Base.openURL(System.getProperty("user.dir") + File.separator +
												"reference" + File.separator + "minicommand-preliminary.pdf");
								   }
								   });
			menu.add(item);
			
			item = new JMenuItem("Visit MidiDuino page");
			item.addActionListener(new ActionListener() {
								   public void actionPerformed(ActionEvent e) {
								   Base.openURL("http://ruinwesen.com/mididuino");
								   }
								   });
			menu.add(item);
			
			item = new JMenuItem("Visit www.arduino.cc");
			item.addActionListener(new ActionListener() {
								   public void actionPerformed(ActionEvent e) {
								   Base.openURL("http://www.arduino.cc/");
								   }
								   });
			menu.add(item);
			
			item = new JMenuItem("Visit ruinwesen.com");
			item.addActionListener(new ActionListener() {
								   public void actionPerformed(ActionEvent e) {
								   Base.openURL("http://www.ruinwesen.com/");
								   }
								   });
			menu.add(item);
			
			// macosx already has its own about menu
			if (!Base.isMacOS()) {
				menu.addSeparator();
				item = new JMenuItem("About Arduino");
				item.addActionListener(new ActionListener() {
															 public void actionPerformed(ActionEvent e) {
															 handleAbout();
															 }
															 });
				menu.add(item);
			}
			
			return menu;
		}
		
		
		public JMenu buildEditMenu() {
			JMenu menu = new JMenu("Edit");
			JMenuItem item;
			
			undoItem = newJMenuItem("Undo", 'Z');
			undoItem.addActionListener(undoAction = new UndoAction());
			menu.add(undoItem);
			
			redoItem = newJMenuItem("Redo", 'Y');
			redoItem.addActionListener(redoAction = new RedoAction());
			menu.add(redoItem);
			
			menu.addSeparator();
			
			// TODO "cut" and "copy" should really only be enabled
			// if some text is currently selected
			item = newJMenuItem("Cut", 'X');
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 textarea.cut();
														 sketch.setModified(true);
														 }
														 });
			menu.add(item);
			
			item = newJMenuItem("Copy", 'C');
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 textarea.copy();
														 }
														 });
			menu.add(item);
			
			item = newJMenuItem("Paste", 'V');
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 textarea.paste();
														 sketch.setModified(true);
														 }
														 });
			menu.add(item);
			
			item = newJMenuItem("Select All", 'A');
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 textarea.selectAll();
														 }
														 });
			menu.add(item);
			
			menu.addSeparator();
			
			item = newJMenuItem("Find...", 'F');
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 if (find == null) {
														 find = new FindReplace(Editor.this);
														 }
														 //new FindReplace(Editor.this).show();
														 find.show();
														 //find.setVisible(true);
														 }
														 });
			menu.add(item);
			
			// TODO find next should only be enabled after a
			// search has actually taken place
			item = newJMenuItem("Find Next", 'G');
			item.addActionListener(new ActionListener() {
														 public void actionPerformed(ActionEvent e) {
														 if (find != null) {
														 //find.find(true);
														 //FindReplace find = new FindReplace(Editor.this); //.show();
														 find.find(true);
														 }
														 }
														 });
			menu.add(item);
			
			return menu;
		}
		
		
		/**
		 * Convenience method, see below.
		 */
		static public JMenuItem newJMenuItem(String title, int what) {
			return newJMenuItem(title, what, false);
		}
		
		
		/**
		 * A software engineer, somewhere, needs to have his abstraction
		 * taken away. In some countries they jail or beat people for writing
		 * the sort of API that would require a five line helper function
		 * just to set the command key for a menu item.
		 */
		static public JMenuItem newJMenuItem(String title,
																				 int what, boolean shift) {
			JMenuItem menuItem = new JMenuItem(title);
			int modifiers = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
			if (shift) modifiers |= ActionEvent.SHIFT_MASK;
			menuItem.setAccelerator(KeyStroke.getKeyStroke(what, modifiers));
			return menuItem;
		}
		
		
		// ...................................................................
		
		
		class UndoAction extends AbstractAction {
			public UndoAction() {
				super("Undo");
				this.setEnabled(false);
			}
			
			public void actionPerformed(ActionEvent e) {
				try {
					undo.undo();
				} catch (CannotUndoException ex) {
					//System.out.println("Unable to undo: " + ex);
					//ex.printStackTrace();
				}
				updateUndoState();
				redoAction.updateRedoState();
			}
			
			protected void updateUndoState() {
				if (undo.canUndo()) {
					this.setEnabled(true);
					undoItem.setEnabled(true);
					undoItem.setText(undo.getUndoPresentationName());
					putValue(Action.NAME, undo.getUndoPresentationName());
					if (sketch != null) {
						sketch.setModified(true);  // 0107
					}
				} else {
					this.setEnabled(false);
					undoItem.setEnabled(false);
					undoItem.setText("Undo");
					putValue(Action.NAME, "Undo");
					if (sketch != null) {
						sketch.setModified(false);  // 0107
					}
				}
			}
		}
		
		
		class RedoAction extends AbstractAction {
			public RedoAction() {
				super("Redo");
				this.setEnabled(false);
			}
			
			public void actionPerformed(ActionEvent e) {
				try {
					undo.redo();
				} catch (CannotRedoException ex) {
					//System.out.println("Unable to redo: " + ex);
					//ex.printStackTrace();
				}
				updateRedoState();
				undoAction.updateUndoState();
			}
			
			protected void updateRedoState() {
				if (undo.canRedo()) {
					redoItem.setEnabled(true);
					redoItem.setText(undo.getRedoPresentationName());
					putValue(Action.NAME, undo.getRedoPresentationName());
				} else {
					this.setEnabled(false);
					redoItem.setEnabled(false);
					redoItem.setText("Redo");
					putValue(Action.NAME, "Redo");
				}
			}
		}
		
		
		// ...................................................................
		
		
		// interfaces for MRJ Handlers, but naming is fine
		// so used internally for everything else
		
		public void handleAbout() {
			final Image image = Base.getImage("about.jpg", this);
			int w = image.getWidth(this);
			int h = image.getHeight(this);
			final Window window = new Window(this) {
        public void paint(Graphics g) {
          g.drawImage(image, 0, 0, null);
					
          Graphics2D g2 = (Graphics2D) g;
          g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                              RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
					
          g.setFont(new Font("SansSerif", Font.PLAIN, 11));
          g.setColor(Color.white);
          g.drawString(Base.VERSION_NAME, 50, 30);
        }
      };
			window.addMouseListener(new MouseAdapter() {
															public void mousePressed(MouseEvent e) {
															window.dispose();
															}
															});
			Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
			window.setBounds((screen.width-w)/2, (screen.height-h)/2, w, h);
			window.show();
		}
		
		
		/**
		 * Show the preferences window.
		 */
		public void handlePrefs() {
			Preferences preferences = new Preferences();
			preferences.showFrame(this);
			
			// since this can't actually block, it'll hide
			// the editor window while the prefs are open
			//preferences.showFrame(this);
			// and then call applyPreferences if 'ok' is hit
			// and then unhide
			
			// may need to rebuild sketch and other menus
			//applyPreferences();
			
			// next have editor do its thing
			//editor.appyPreferences();
		}
		
		
		// ...................................................................
		
		
		/**
		 * Get the contents of the current buffer. Used by the Sketch class.
		 */
		public String getText() {
			return textarea.getText();
		}
		
		
		/**
		 * Called to update the text but not switch to a different
		 * set of code (which would affect the undo manager).
		 */
		public void setText(String what, int selectionStart, int selectionEnd) {
			beginCompoundEdit();
			textarea.setText(what);
			endCompoundEdit();
			
			// make sure that a tool isn't asking for a bad location
			selectionStart =
      Math.max(0, Math.min(selectionStart, textarea.getDocumentLength()));
			selectionEnd =
      Math.max(0, Math.min(selectionStart, textarea.getDocumentLength()));
			textarea.select(selectionStart, selectionEnd);
			
			textarea.requestFocus();  // get the caret blinking
		}
		
		
		/**
		 * Switch between tabs, this swaps out the Document object
		 * that's currently being manipulated.
		 */
		public void setCode(SketchCode code) {
			if (code.document == null) {  // this document not yet inited
				code.document = new SyntaxDocument();
				
				// turn on syntax highlighting
				code.document.setTokenMarker(new PdeKeywords());
				
				// insert the program text into the document object
				try {
					code.document.insertString(0, code.program, null);
				} catch (BadLocationException bl) {
					bl.printStackTrace();
				}
				
				// set up this guy's own undo manager
				code.undo = new UndoManager();
				
				// connect the undo listener to the editor
				code.document.addUndoableEditListener(new UndoableEditListener() {
																							public void undoableEditHappened(UndoableEditEvent e) {
																							if (compoundEdit != null) {
																							compoundEdit.addEdit(e.getEdit());
																							
																							} else if (undo != null) {
																							undo.addEdit(e.getEdit());
																							undoAction.updateUndoState();
																							redoAction.updateRedoState();
																							}
																							}
																							});
			}
			
			// update the document object that's in use
			textarea.setDocument(code.document,
													 code.selectionStart, code.selectionStop,
													 code.scrollPosition);
			
			textarea.requestFocus();  // get the caret blinking
			
			this.undo = code.undo;
			undoAction.updateUndoState();
			redoAction.updateRedoState();
		}
		
		
		public void beginCompoundEdit() {
			compoundEdit = new CompoundEdit();
		}
		
		
		public void endCompoundEdit() {
			compoundEdit.end();
			undo.addEdit(compoundEdit);
			undoAction.updateUndoState();
			redoAction.updateRedoState();
			compoundEdit = null;
		}
		
		
		// ...................................................................
		
		
		public void handleRun(final boolean present) {
			doClose();
			running = true;
			buttons.activate(EditorButtons.RUN);
			message("Compiling...");
			// do this for the terminal window / dos prompt / etc
			for (int i = 0; i < 10; i++) System.out.println();
			
			// clear the console on each run, unless the user doesn't want to
			//if (Base.getBoolean("console.auto_clear", true)) {
			//if (Preferences.getBoolean("console.auto_clear", true)) {
			if (Preferences.getBoolean("console.auto_clear")) {
				console.clear();
			}
			
			presenting = present;
			if (presenting && Base.isMacOS()) {
				// check to see if osx 10.2, if so, show a warning
				String osver = System.getProperty("os.version").substring(0, 4);
				if (osver.equals("10.2")) {
					Base.showWarning("Time for an OS Upgrade",
													 "The \"Present\" feature may not be available on\n" +
													 "Mac OS X 10.2, because of what appears to be\n" +
													 "a bug in the Java 1.4 implementation on 10.2.\n" +
													 "In case it works on your machine, present mode\n" +
													 "will start, but if you get a flickering white\n" +
													 "window, using Command-Q to quit the sketch", null);
				}
			}
			
			SwingUtilities.invokeLater(new Runnable() {
																 public void run() {
																 try {
																 if (!sketch.handleRun(new Target(
																																	System.getProperty("user.dir") + File.separator + "hardware" +
																																	File.separator + "cores",
																																	Preferences.get("boards." + Preferences.get("board") + ".build.core"))))
																 return;
																 
																 //runtime = new Runner(sketch, Editor.this);
																 //runtime.start(appletLocation);
																 watcher = new RunButtonWatcher();
																 message("Done compiling.");
																 if(watcher != null) watcher.stop();
																 
																 } catch (RunnerException e) {
																 message("Error compiling...");
																 error(e);
																 
																 } catch (Exception e) {
																 e.printStackTrace();
																 }
																 }});
			
			// this doesn't seem to help much or at all
			/*
			 final SwingWorker worker = new SwingWorker() {
			 public Object construct() {
			 try {
			 if (!sketch.handleRun()) return null;
			 
			 runtime = new Runner(sketch, Editor.this);
			 runtime.start(presenting ? presentLocation : appletLocation);
			 watcher = new RunButtonWatcher();
			 message("Done compiling.");
			 
			 } catch (RunnerException e) {
			 message("Error compiling...");
			 error(e);
			 
			 } catch (Exception e) {
			 e.printStackTrace();
			 }
			 return null;  // needn't return anything
			 }
			 };
			 worker.start();
			 */
			//sketch.cleanup();  // where does this go?
			buttons.clear();
		}
		
		
		class RunButtonWatcher implements Runnable {
			Thread thread;
			
			public RunButtonWatcher() {
				thread = new Thread(this, "run button watcher");
				thread.setPriority(Thread.MIN_PRIORITY);
				thread.start();
			}
			
			public void run() {
				while (Thread.currentThread() == thread) {
					/*if (runtime == null) {
					 stop();
					 
					 } else {
					 if (runtime.applet != null) {
					 if (runtime.applet.finished) {
					 stop();
					 }
					 //buttons.running(!runtime.applet.finished);
					 
					 } else if (runtime.process != null) {
					 //buttons.running(true);  // ??
					 
					 } else {
					 stop();
					 }
					 }*/
					try {
						Thread.sleep(250);
					} catch (InterruptedException e) { }
					//System.out.println("still inside runner thread");
				}
			}
			
			public void stop() {
				buttons.running(false);
				thread = null;
			}
		}
		
		
		public void handleSerial() {
			if (!debugging) {
				try {
					serialPort = new Serial(true);
					console.clear();
					buttons.activate(EditorButtons.SERIAL);
					debugging = true;
					status.serial();
				} catch(SerialException e) {
					error(e);
				}
			} else {
				doStop();
			}
		}
		
		
		public void handleStop() {  // called by menu or buttons
			if (presenting) {
				doClose();
			} else {
				doStop();
			}
			buttons.clear();
		}
		
		
		/**
		 * Stop the applet but don't kill its window.
		 */
		public void doStop() {
			//if (runtime != null) runtime.stop();
			if (debugging)  {
				status.unserial();
				serialPort.dispose();
				debugging = false;
			}
			if (watcher != null) watcher.stop();
			message(EMPTY);
			
			// the buttons are sometimes still null during the constructor
			// is this still true? are people still hitting this error?
			/*if (buttons != null)*/ buttons.clear();
			
			running = false;
		}
		
		
		/**
		 * Stop the applet and kill its window. When running in presentation
		 * mode, this will always be called instead of doStop().
		 */
		public void doClose() {
			//if (presenting) {
			//presentationWindow.hide();
			//} else {
			//try {
      // the window will also be null the process was running
      // externally. so don't even try setting if window is null
      // since Runner will set the appletLocation when an
      // external process is in use.
			//      if (runtime.window != null) {
			//        appletLocation = runtime.window.getLocation();
			//      }
			//} catch (NullPointerException e) { }
			//}
			
			//if (running) doStop();
			doStop();  // need to stop if runtime error
			
			//try {
      /*if (runtime != null) {
			 runtime.close();  // kills the window
			 runtime = null; // will this help?
			 }*/
			//} catch (Exception e) { }
			//buttons.clear();  // done by doStop
			
			sketch.cleanup();
			
			// [toxi 030903]
			// focus the PDE again after quitting presentation mode
			toFront();
		}
		
		
		/**
		 * Check to see if there have been changes. If so, prompt user
		 * whether or not to save first. If the user cancels, just ignore.
		 * Otherwise, one of the other methods will handle calling
		 * checkModified2() which will get on with business.
		 */
		protected void checkModified(int checkModifiedMode) {
			this.checkModifiedMode = checkModifiedMode;
			
			if (!sketch.modified) {
				checkModified2();
				return;
			}
			
			String prompt = "Save changes to " + sketch.name + "?  ";
			
			if (checkModifiedMode != HANDLE_QUIT) {
				// if the user is not quitting, then use simpler nicer
				// dialog that's actually inside the p5 window.
				status.prompt(prompt);
				
			} else {
				if (!Base.isMacOS() || PApplet.javaVersion < 1.5f) {
					int result =
          JOptionPane.showConfirmDialog(this, prompt, "Quit",
                                        JOptionPane.YES_NO_CANCEL_OPTION,
                                        JOptionPane.QUESTION_MESSAGE);
					
					if (result == JOptionPane.YES_OPTION) {
						handleSave(true);
						checkModified2();
						
					} else if (result == JOptionPane.NO_OPTION) {
						checkModified2();
					}
					// cancel is ignored altogether
					
				} else {
					// This code is disabled unless Java 1.5 is being used on Mac OS X
					// because of a Java bug that prevents the initial value of the
					// dialog from being set properly (at least on my MacBook Pro).
					// The bug causes the "Don't Save" option to be the highlighted,
					// blinking, default. This sucks. But I'll tell you what doesn't
					// suck--workarounds for the Mac and Apple's snobby attitude about it!
					
					// adapted from the quaqua guide
					// http://www.randelshofer.ch/quaqua/guide/joptionpane.html
					JOptionPane pane =
          new JOptionPane("<html> " +
                          "<head> <style type=\"text/css\">"+
                          "b { font: 13pt \"Lucida Grande\" }"+
                          "p { font: 11pt \"Lucida Grande\"; margin-top: 8px }"+
                          "</style> </head>" +
                          "<b>Do you want to save changes to this sketch<BR>" +
                          " before closing?</b>" +
                          "<p>If you don't save, your changes will be lost.",
                          JOptionPane.QUESTION_MESSAGE);
					
					String[] options = new String[] {
						"Save", "Cancel", "Don't Save"
					};
					pane.setOptions(options);
					
					// highlight the safest option ala apple hig
					pane.setInitialValue(options[0]);
					
					// on macosx, setting the destructive property places this option
					// away from the others at the lefthand side
					pane.putClientProperty("Quaqua.OptionPane.destructiveOption",
																 new Integer(2));
					
					JDialog dialog = pane.createDialog(this, null);
					dialog.show();
					
					Object result = pane.getValue();
					if (result == options[0]) {  // save (and quit)
						handleSave(true);
						checkModified2();
						
					} else if (result == options[2]) {  // don't save (still quit)
						checkModified2();
					}
				}
			}
		}
		
		
		/**
		 * Called by EditorStatus to complete the job and re-dispatch
		 * to handleNew, handleOpen, handleQuit.
		 */
		public void checkModified2() {
			switch (checkModifiedMode) {
					case HANDLE_NEW:  handleNew2(false); break;
					case HANDLE_OPEN: handleOpen2(handleOpenPath); break;
					case HANDLE_QUIT: handleQuit2(); break;
			}
			checkModifiedMode = 0;
		}
		
		
		/**
		 * New was called (by buttons or by menu), first check modified
		 * and if things work out ok, handleNew2() will be called.
		 * <p/>
		 * If shift is pressed when clicking the toolbar button, then
		 * force the opposite behavior from sketchbook.prompt's setting
		 */
		public void handleNew(final boolean shift) {
			buttons.activate(EditorButtons.NEW);
			
			SwingUtilities.invokeLater(new Runnable() {
																 public void run() {
																 doStop();
																 handleNewShift = shift;
																 handleNewLibrary = false;
																 checkModified(HANDLE_NEW);
																 }});
		}
		
		
		/**
		 * Extra public method so that Sketch can call this when a sketch
		 * is selected to be deleted, and it won't call checkModified()
		 * to prompt for save as.
		 */
		public void handleNewUnchecked() {
			doStop();
			handleNewShift = false;
			handleNewLibrary = false;
			handleNew2(true);
		}
		
		
		/**
		 * User selected "New Library", this will act just like handleNew
		 * but internally set a flag that the new guy is a library,
		 * meaning that a "library" subfolder will be added.
		 */
		public void handleNewLibrary() {
			doStop();
			handleNewShift = false;
			handleNewLibrary = true;
			checkModified(HANDLE_NEW);
		}
		
		
		/**
		 * Does all the plumbing to create a new project
		 * then calls handleOpen to load it up.
		 *
		 * @param noPrompt true to disable prompting for the sketch
		 * name, used when the app is starting (auto-create a sketch)
		 */
		protected void handleNew2(boolean noPrompt) {
			try {
				String pdePath =
        sketchbook.handleNew(noPrompt, handleNewShift, handleNewLibrary);
				if (pdePath != null) handleOpen2(pdePath);
				
			} catch (IOException e) {
				// not sure why this would happen, but since there's no way to
				// recover (outside of creating another new setkch, which might
				// just cause more trouble), then they've gotta quit.
				Base.showError("Problem creating a new sketch",
											 "An error occurred while creating\n" +
											 "a new sketch. Arduino must now quit.", e);
			}
			buttons.clear();
		}
		
		
		/**
		 * This is the implementation of the MRJ open document event,
		 * and the Windows XP open document will be routed through this too.
		 */
		public void handleOpenFile(File file) {
			//System.out.println("handling open file: " + file);
			handleOpen(file.getAbsolutePath());
		}
		
		
		/**
		 * Open a sketch given the full path to the .pde file.
		 * Pass in 'null' to prompt the user for the name of the sketch.
		 */
		public void handleOpen(final String ipath) {
			// haven't run across a case where i can verify that this works
			// because open is usually very fast.
			buttons.activate(EditorButtons.OPEN);
			
			SwingUtilities.invokeLater(new Runnable() {
																 public void run() {
																 String path = ipath;
																 if (path == null) {  // "open..." selected from the menu
																 path = sketchbook.handleOpen();
																 if (path == null) return;
																 }
																 doClose();
																 handleOpenPath = path;
																 checkModified(HANDLE_OPEN);
																 }});
		}
		
		
		/**
		 * Open a sketch from a particular path, but don't check to save changes.
		 * Used by Sketch.saveAs() to re-open a sketch after the "Save As"
		 */
		public void handleOpenUnchecked(String path, int codeIndex,
																		int selStart, int selStop, int scrollPos) {
			doClose();
			handleOpen2(path);
			
			sketch.setCurrent(codeIndex);
			textarea.select(selStart, selStop);
			//textarea.updateScrollBars();
			textarea.setScrollPosition(scrollPos);
		}
		
		
		/**
		 * Second stage of open, occurs after having checked to
		 * see if the modifications (if any) to the previous sketch
		 * need to be saved.
		 */
		protected void handleOpen2(String path) {
			if (sketch != null) {
				// if leaving an empty sketch (i.e. the default) do an
				// auto-clean right away
				try {
					// don't clean if we're re-opening the same file
					String oldPath = sketch.code[0].file.getCanonicalPath();
					String newPath = new File(path).getCanonicalPath();
					if (!oldPath.equals(newPath)) {
						if (Base.calcFolderSize(sketch.folder) == 0) {
							Base.removeDir(sketch.folder);
							//sketchbook.rebuildMenus();
							sketchbook.rebuildMenusAsync();
						}
					}
				} catch (Exception e) { }   // oh well
			}
			
			try {
				// check to make sure that this .pde file is
				// in a folder of the same name
				File file = new File(path);
				File parentFile = new File(file.getParent());
				String parentName = parentFile.getName();
				String pdeName = parentName + ".pde";
				File altFile = new File(file.getParent(), pdeName);
				
				//System.out.println("path = " + file.getParent());
				//System.out.println("name = " + file.getName());
				//System.out.println("pname = " + parentName);
				
				if (pdeName.equals(file.getName())) {
					// no beef with this guy
					
				} else if (altFile.exists()) {
					// user selected a .java from the same sketch,
					// but open the .pde instead
					path = altFile.getAbsolutePath();
					//System.out.println("found alt file in same folder");
					
				} else if (!path.endsWith(".pde")) {
					Base.showWarning("Bad file selected",
													 "Arduino can only open its own sketches\n" +
													 "and other files ending in .pde", null);
					return;
					
				} else {
					String properParent =
          file.getName().substring(0, file.getName().length() - 4);
					
					Object[] options = { "OK", "Cancel" };
					String prompt =
          "The file \"" + file.getName() + "\" needs to be inside\n" +
          "a sketch folder named \"" + properParent + "\".\n" +
          "Create this folder, move the file, and continue?";
					
					int result = JOptionPane.showOptionDialog(this,
																										prompt,
																										"Moving",
																										JOptionPane.YES_NO_OPTION,
																										JOptionPane.QUESTION_MESSAGE,
																										null,
																										options,
																										options[0]);
					
					if (result == JOptionPane.YES_OPTION) {
						// create properly named folder
						File properFolder = new File(file.getParent(), properParent);
						if (properFolder.exists()) {
							Base.showWarning("Error",
															 "A folder named \"" + properParent + "\" " +
															 "already exists. Can't open sketch.", null);
							return;
						}
						if (!properFolder.mkdirs()) {
							throw new IOException("Couldn't create sketch folder");
						}
						// copy the sketch inside
						File properPdeFile = new File(properFolder, file.getName());
						File origPdeFile = new File(path);
						Base.copyFile(origPdeFile, properPdeFile);
						
						// remove the original file, so user doesn't get confused
						origPdeFile.delete();
						
						// update with the new path
						path = properPdeFile.getAbsolutePath();
						
					} else if (result == JOptionPane.NO_OPTION) {
						return;
					}
				}
				
				sketch = new Sketch(this, path);
				// TODO re-enable this once export application works
				//exportAppItem.setEnabled(false);
				//exportAppItem.setEnabled(false && !sketch.isLibrary());
				//buttons.disableRun(sketch.isLibrary());
				header.rebuild();
				if (Preferences.getBoolean("console.auto_clear")) {
					console.clear();
				}
				
			} catch (Exception e) {
				error(e);
			}
		}
		
		
		// there is no handleSave1 since there's never a need to prompt
		/**
		 * Actually handle the save command. If 'force' is set to false,
		 * this will happen in another thread so that the message area
		 * will update and the save button will stay highlighted while the
		 * save is happening. If 'force' is true, then it will happen
		 * immediately. This is used during a quit, because invokeLater()
		 * won't run properly while a quit is happening. This fixes
		 * <A HREF="http://dev.processing.org/bugs/show_bug.cgi?id=276">Bug 276</A>.
		 */
		public void handleSave(boolean force) {
			doStop();
			buttons.activate(EditorButtons.SAVE);
			
			if (force) {
				handleSave2();
			} else {
				SwingUtilities.invokeLater(new Runnable() {
																	 public void run() {
																	 handleSave2();
																	 }
																	 });
			}
		}
		
		
		public void handleSave2() {
			message("Saving...");
			try {
				if (sketch.save()) {
					message("Done Saving.");
				} else {
					message(EMPTY);
				}
				// rebuild sketch menu in case a save-as was forced
				// Disabling this for 0125, instead rebuild the menu inside
				// the Save As method of the Sketch object, since that's the
				// only one who knows whether something was renamed.
				//sketchbook.rebuildMenus();
				//sketchbook.rebuildMenusAsync();
				
			} catch (Exception e) {
				// show the error as a message in the window
				error(e);
				
				// zero out the current action,
				// so that checkModified2 will just do nothing
				checkModifiedMode = 0;
				// this is used when another operation calls a save
			}
			buttons.clear();
		}
		
		
		public void handleSaveAs() {
			doStop();
			buttons.activate(EditorButtons.SAVE);
			
			SwingUtilities.invokeLater(new Runnable() {
																 public void run() {
																 message("Saving...");
																 try {
																 if (sketch.saveAs()) {
																 message("Done Saving.");
																 // Disabling this for 0125, instead rebuild the menu inside
																 // the Save As method of the Sketch object, since that's the
																 // only one who knows whether something was renamed.
																 //sketchbook.rebuildMenusAsync();
																 } else {
																 message("Save Cancelled.");
																 }
																 } catch (Exception e) {
																 // show the error as a message in the window
																 error(e);
																 }
																 buttons.clear();
																 }});
		}
		
		
		/**
		 * Handles calling the export() function on sketch, and
		 * queues all the gui status stuff that comes along with it.
		 *
		 * Made synchronized to (hopefully) avoid problems of people
		 * hitting export twice, quickly, and horking things up.
		 */
		synchronized public void handleExport() {
			if(debugging)
				doStop();
			buttons.activate(EditorButtons.EXPORT);
			console.clear();
			//String what = sketch.isLibrary() ? "Applet" : "Library";
			//message("Exporting " + what + "...");
			message("Uploading to I/O Board...");
			
			SwingUtilities.invokeLater(new Runnable() {
																 public void run() {
																 try {
																 //boolean success = sketch.isLibrary() ?
																 //sketch.exportLibrary() : sketch.exportApplet();
																 boolean success = sketch.exportApplet(new Target(
																																									System.getProperty("user.dir") + File.separator + "hardware" +
																																									File.separator + "cores",
																																									Preferences.get("boards." + Preferences.get("board") + ".build.core")));
																 if (success) {
																 message("Done uploading.");
																 } else {
																 // error message will already be visible
																 }
																 } catch (RunnerException e) {
																 message("Error during upload.");
																 //e.printStackTrace();
																 error(e);
																 } catch (Exception e) {
																 e.printStackTrace();
																 }
																 buttons.clear();
																 }});
		}
		
		
		synchronized public void handleExportApp() {
			message("Exporting application...");
			try {
				if (sketch.exportApplication()) {
					message("Done exporting.");
				} else {
					// error message will already be visible
				}
			} catch (Exception e) {
				message("Error during export.");
				e.printStackTrace();
			}
			buttons.clear();
		}
		
		
		/**
		 * Checks to see if the sketch has been modified, and if so,
		 * asks the user to save the sketch or cancel the export.
		 * This prevents issues where an incomplete version of the sketch
		 * would be exported, and is a fix for
		 * <A HREF="http://dev.processing.org/bugs/show_bug.cgi?id=157">Bug 157</A>
		 */
		public boolean handleExportCheckModified() {
			if (!sketch.modified) return true;
			
			Object[] options = { "OK", "Cancel" };
			int result = JOptionPane.showOptionDialog(this,
																								"Save changes before export?",
																								"Save",
																								JOptionPane.OK_CANCEL_OPTION,
																								JOptionPane.QUESTION_MESSAGE,
																								null,
																								options,
																								options[0]);
			
			if (result == JOptionPane.OK_OPTION) {
				handleSave(true);
				
			} else {
				// why it's not CANCEL_OPTION is beyond me (at least on the mac)
				// but f-- it.. let's get this shite done..
				//} else if (result == JOptionPane.CANCEL_OPTION) {
				message("Export canceled, changes must first be saved.");
				buttons.clear();
				return false;
			}
			return true;
		}
		
		
		public void handlePageSetup() {
			//printerJob = null;
			if (printerJob == null) {
				printerJob = PrinterJob.getPrinterJob();
			}
			if (pageFormat == null) {
				pageFormat = printerJob.defaultPage();
			}
			pageFormat = printerJob.pageDialog(pageFormat);
			//System.out.println("page format is " + pageFormat);
		}
		
		
		public void handlePrint() {
			message("Printing...");
			//printerJob = null;
			if (printerJob == null) {
				printerJob = PrinterJob.getPrinterJob();
			}
			if (pageFormat != null) {
				//System.out.println("setting page format " + pageFormat);
				printerJob.setPrintable(textarea.getPainter(), pageFormat);
			} else {
				printerJob.setPrintable(textarea.getPainter());
			}
			// set the name of the job to the code name
			printerJob.setJobName(sketch.current.name);
			
			if (printerJob.printDialog()) {
				try {
					printerJob.print();
					message("Done printing.");
					
				} catch (PrinterException pe) {
					error("Error while printing.");
					pe.printStackTrace();
				}
			} else {
				message("Printing canceled.");
			}
			//printerJob = null;  // clear this out?
		}
		
		
		/**
		 * Quit, but first ask user if it's ok. Also store preferences
		 * to disk just in case they want to quit. Final exit() happens
		 * in Editor since it has the callback from EditorStatus.
		 */
		public void handleQuitInternal() {
			// doStop() isn't sufficient with external vm & quit
			// instead use doClose() which will kill the external vm
			doClose();
			
			checkModified(HANDLE_QUIT);
		}
		
		
		/**
		 * Method for the MRJQuitHandler, needs to be dealt with differently
		 * than the regular handler because OS X has an annoying implementation
		 * <A HREF="http://developer.apple.com/qa/qa2001/qa1187.html">quirk</A>
		 * that requires an exception to be thrown in order to properly cancel
		 * a quit message.
		 */
		public void handleQuit() {
			SwingUtilities.invokeLater(new Runnable() {
																 public void run() {
																 handleQuitInternal();
																 }
																 });
			
			// Throw IllegalStateException so new thread can execute.
			// If showing dialog on this thread in 10.2, we would throw
			// upon JOptionPane.NO_OPTION
			throw new IllegalStateException("Quit Pending User Confirmation");
		}
		
		
		/**
		 * Actually do the quit action.
		 */
		protected void handleQuit2() {
			storePreferences();
			Preferences.save();
			
			sketchbook.clean();
			console.handleQuit();
			
			//System.out.println("exiting here");
			System.exit(0);
		}
		
		
		
		protected void handleReference() {
			String text = textarea.getSelectedText().trim();
			
			if (text.length() == 0) {
				message("First select a word to find in the reference.");
				
			} else {
				String referenceFile = PdeKeywords.getReference(text);
				//System.out.println("reference file is " + referenceFile);
				if (referenceFile == null) {
					message("No reference available for \"" + text + "\"");
				} else {
					Base.showReference(referenceFile + ".html");
				}
			}
		}
		
		protected void handleBurnBootloader(final String programmer) {
			if(debugging)
				doStop();
			console.clear();
			message("Burning bootloader to I/O Board (this may take a minute)...");
			SwingUtilities.invokeLater(new Runnable() {
																 public void run() {
																 try {
																 Uploader uploader = new AvrdudeUploader();
																 if (uploader.burnBootloader(programmer)) {
																 message("Done burning bootloader.");
																 } else {
																 // error message will already be visible
																 }
																 } catch (RunnerException e) {
																 message("Error while burning bootloader.");
																 //e.printStackTrace();
																 error(e);
																 } catch (Exception e) {
																 e.printStackTrace();
																 }
																 buttons.clear();
																 }});
		}
		
		public void highlightLine(int lnum) {
			if (lnum < 0) {
				textarea.select(0, 0);
				return;
			}
			//System.out.println(lnum);
			String s = textarea.getText();
			int len = s.length();
			int st = -1;
			int ii = 0;
			int end = -1;
			int lc = 0;
			if (lnum == 0) st = 0;
			for (int i = 0; i < len; i++) {
				ii++;
				//if ((s.charAt(i) == '\n') || (s.charAt(i) == '\r')) {
				boolean newline = false;
				if (s.charAt(i) == '\r') {
					if ((i != len-1) && (s.charAt(i+1) == '\n')) {
						i++; //ii--;
					}
					lc++;
					newline = true;
				} else if (s.charAt(i) == '\n') {
					lc++;
					newline = true;
				}
				if (newline) {
					if (lc == lnum)
						st = ii;
					else if (lc == lnum+1) {
						//end = ii;
						// to avoid selecting entire, because doing so puts the
						// cursor on the next line [0090]
						end = ii - 1;
						break;
					}
				}
			}
			if (end == -1) end = len;
			
			// sometimes KJC claims that the line it found an error in is
			// the last line in the file + 1.  Just highlight the last line
			// in this case. [dmose]
			if (st == -1) st = len;
			
			textarea.select(st, end);
		}
		
		
		// ...................................................................
		
		
		/**
		 * Show an error int the status bar.
		 */
		public void error(String what) {
			status.error(what);
		}
		
		
		public void error(Exception e) {
			if (e == null) {
				System.err.println("Editor.error() was passed a null exception.");
				return;
			}
			
			// not sure if any RuntimeExceptions will actually arrive
			// through here, but gonna check for em just in case.
			String mess = e.getMessage();
			if (mess != null) {
				String rxString = "RuntimeException: ";
				if (mess.indexOf(rxString) == 0) {
					mess = mess.substring(rxString.length());
				}
				String javaLang = "java.lang.";
				if (mess.indexOf(javaLang) == 0) {
					mess = mess.substring(javaLang.length());
				}
				error(mess);
			}
			e.printStackTrace();
		}
		
		
		public void error(RunnerException e) {
			//System.out.println("file and line is " + e.file + " " + e.line);
			if (e.file >= 0) sketch.setCurrent(e.file);
			if (e.line >= 0) highlightLine(e.line);
			
			// remove the RuntimeException: message since it's not
			// really all that useful to the user
			//status.error(e.getMessage());
			String mess = e.getMessage();
			String rxString = "RuntimeException: ";
			if (mess.indexOf(rxString) == 0) {
				mess = mess.substring(rxString.length());
				//System.out.println("MESS3: " + mess);
			}
			String javaLang = "java.lang.";
			if (mess.indexOf(javaLang) == 0) {
				mess = mess.substring(javaLang.length());
			}
			error(mess);
			buttons.clear();
		}
		
		
		
		public void message(String msg) {
			status.notice(msg);
		}
		
		
		
		// ...................................................................
		
		
		/**
		 * Returns the edit popup menu.
		 */
		class TextAreaPopup extends JPopupMenu {
			//String currentDir = System.getProperty("user.dir");
			String referenceFile = null;
			
			JMenuItem cutItem, copyItem;
			JMenuItem referenceItem;
			
			
			public TextAreaPopup() {
				JMenuItem item;
				
				cutItem = new JMenuItem("Cut");
				cutItem.addActionListener(new ActionListener() {
																	public void actionPerformed(ActionEvent e) {
																	textarea.cut();
																	sketch.setModified(true);
																	}
																	});
				this.add(cutItem);
				
				copyItem = new JMenuItem("Copy");
				copyItem.addActionListener(new ActionListener() {
																	 public void actionPerformed(ActionEvent e) {
																	 textarea.copy();
																	 }
																	 });
				this.add(copyItem);
				
				item = new JMenuItem("Paste");
				item.addActionListener(new ActionListener() {
															 public void actionPerformed(ActionEvent e) {
															 textarea.paste();
															 sketch.setModified(true);
															 }
															 });
				this.add(item);
				
				item = new JMenuItem("Select All");
				item.addActionListener(new ActionListener() {
															 public void actionPerformed(ActionEvent e) {
															 textarea.selectAll();
															 }
															 });
				this.add(item);
				
				this.addSeparator();
				
				referenceItem = new JMenuItem("Find in Reference");
				referenceItem.addActionListener(new ActionListener() {
																				public void actionPerformed(ActionEvent e) {
																				//Base.showReference(referenceFile + ".html");
																				handleReference(); //textarea.getSelectedText());
																				}
																				});
				this.add(referenceItem);
			}
			
			// if no text is selected, disable copy and cut menu items
			public void show(Component component, int x, int y) {
				if (textarea.isSelectionActive()) {
					cutItem.setEnabled(true);
					copyItem.setEnabled(true);
					
					String sel = textarea.getSelectedText().trim();
					referenceFile = PdeKeywords.getReference(sel);
					referenceItem.setEnabled(referenceFile != null);
					
				} else {
					cutItem.setEnabled(false);
					copyItem.setEnabled(false);
					referenceItem.setEnabled(false);
				}
				super.show(component, x, y);
			}
		}
	}

